"use strict";

// demo地址 https://webrtc-from-chat.glitch.me/

// WebSocket聊天/信令通道变量。
var connection = null;
var clientID = 0;

/*
  媒体约束对象描述了我们想要什么样的流
  从本地的A/V硬件(通常是一个网络摄像头和麦克风)。
  还可以使用(或要求)特定分辨率的视频，
  是选择面向用户的摄像头，还是选择后置摄像头(如果有的话)，等等。
  参见:
    https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints
    https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
 */
var mediaConstraints = {
  audio: true,
  video: true
};

var myUsername = null;
var targetUsername = null;      // 存储其他对等点的用户名
var myPeerConnection = null;    // RTC对等连接

// 不管用不用addTrack()，我们需要注意它是否可用
var hasAddTrack = false;


//==========================================webscoket=============================================================================================
/*

  websocket 内容

*/
// 打开并配置到WebSocket服务器的连接。
function connect() {
  connection = new WebSocket('wss://localhost:6503/', "json");
  // 连接建立时自动触发
  connection.onopen = function(evt) {
    document.getElementById("text").disabled = false;
    document.getElementById("send").disabled = false;
  };
  // 通信发生错误时触发
  connection.onerror = function(evt) {
    console.dir(evt);
  }
  // 连接关闭时触发
  connection.onclose = function(evt) {
    console.log('关闭'+evt);
  }
  // 客户端接收服务端数据时触发
  connection.onmessage = function(evt) {
    var chatFrameDocument = document.getElementById("chatbox");
    var text = "";
    var msg = JSON.parse(evt.data);
    console.log(msg);
    log("Message received: ");
    console.dir(msg);
    var time = new Date(msg.date);
    var timeStr = time.toLocaleTimeString();
    switch(msg.type) {
      case "id":
        clientID = msg.id;
        setUsername();
        break;
      case "username":
        text = "<b>User <em>" + msg.name + "</em> signed in at " + timeStr + "</b><br>";
        break;
      case "message":
        text = "(" + timeStr + ") <b>" + msg.name + "</b>: " + msg.text + "<br>";
        break;
      case "rejectusername":
        myUsername = msg.name;
        text = "<b>Your username has been set to <em>" + myUsername +
          "</em> because the name you chose is in use.</b><br>";
        break;
      case "userlist":      // 收到更新的用户列表
        handleUserlistMsg(msg);
        break;
      // 信令消息:这些消息用于在视频通话之前的谈判期间交换WebRTC信令信息。
      case "video-offer":  // 发送 offer
        handleVideoOfferMsg(msg);
        break;
      case "apply-video":  // 收到聊天邀请
        handleApplyVideoMsg(msg);
        break;
      case "reply-video":  // 收到邀请回复
        handleReplyVideoMsg(msg);
        break;
      case "video-answer":  // Callee已经答复了我们的报价
        handleVideoAnswerMsg(msg);
        break;
      case "new-ice-candidate": // 一位新的ICE候选人已经收到
        handleNewICECandidateMsg(msg);
        break;
      case "hang-up": // 另一位同事挂断了电话
        handleHangUpMsg(msg);
        break;
      // 未知的信息;输出到控制台进行调试。
      default:
        log_error("未知的信息收到了:");
        log_error(msg);
    }

    // 如果有文本要插入聊天缓冲区，现在就插入，然后滚动聊天面板，这样就可以看到新文本。
    if (text.length) {
      // chatFrameDocument.write(text);
      document.getElementById("chatbox").innerHTML += `<p>${text}</p>`;
    }
  };
}

// 给定包含用户名列表的消息，此函数将用这些名称填充用户列表框，使每个项都可单击，从而允许启动视频调用。
function handleUserlistMsg(msg) {
  var i;
  var listElem = document.getElementById("userlistbox");
  // 删除所有当前列表成员。我们可以更聪明地做到这一点，通过添加和更新用户，而不是从头开始重新构建，但这将对这个示例有效。
  while (listElem.firstChild) {
    listElem.removeChild(listElem.firstChild);
  }
  // 从接收列表中添加成员名
  for (i=0; i < msg.users.length; i++) {
    var item = document.createElement("li");
    item.appendChild(document.createTextNode(msg.users[i]));
    item.addEventListener("click", invite, false);
    listElem.appendChild(item);
  }
}

// 将 JavaScript 对象转换为 JSON 并在WebSocket连接上作为消息发送。
function sendToServer(msg) {
  var msgJSON = JSON.stringify(msg);
  log("Sending '" + msg.type + "' message: " + msgJSON);
  connection.send(msgJSON);
}

/*
  在接收到“id”消息时调用;此消息由服务器发送，以分配此登录会话的唯一ID号;
  作为响应，这个函数发送一个“username”消息来设置会话的用户名。
*/
function setUsername() {
  myUsername = document.getElementById("name").value;
  sendToServer({
    name: myUsername,
    date: Date.now(),
    id: clientID,
    type: "username"
  });
}

// 通过构建一个“message”对象并将其发送到服务器来处理单击Send按钮(或按return/enter)。
function handleSendButton() {
  var msg = {
    text: document.getElementById("text").value,
    type: "message",
    id: clientID,
    date: Date.now()
  };
  sendToServer(msg);
  document.getElementById("text").value = "";
}

// 键盘事件处理程序。这用于拦截返回和输入键，以便我们可以调用send()将输入的文本传输到服务器。
function handleKey(evt) {
  if (evt.keyCode === 13 || evt.keyCode === 14) {
    if (!document.getElementById("send").disabled) {
      handleSendButton();
    }
  }
}


//======================================webRTC==========================================================================
/*
  通过邀请被单击的用户进行视频聊天来处理对用户列表中某项的单击。
  注意，我们实际上并没有向这里的被调用者发送消息——调用RTCPeerConnection.addStream()发出一个| notificationrequired |事件，因此我们将让处理程序发出该通知。
*/
function invite(evt) {
  log("开始准备邀请函");
  if (myPeerConnection) {
    alert("你不能打电话，因为你已经有一个空电话了!");
  } else {
    var clickedUsername = evt.target.textContent;
    // 不允许用户称呼自己，因为很奇怪。
    if (clickedUsername === myUsername) {
      alert("恐怕我不能让你自言自语。那太奇怪了。");
      return;
    }
    // 记录被调用的用户名，以备将来参考
    targetUsername = clickedUsername;
    log("邀请用户 " + targetUsername);
    // 发送请求并等待对方同意
    sendToServer({
      name: myUsername,
      target: targetUsername,
      id: clientID,
      type: "apply-video"
    });
  }
}

// 收到邀请消息并处理
function handleApplyVideoMsg(msg) {
  var targetUsername = msg.name;
  // 如果正在通话中
  if(myPeerConnection) {
    // 通知对方回复消息
    reply(targetUsername, 3);
    return;
  }
  // 在提示是否允许
  if (confirm(targetUsername+"向你请求视频通话, 是否同意?")){
    handleReplyFun(msg)
  }else{
    reply(targetUsername,2);
  }
}

/*
  请求处理回复
  replyType 取值
    1 表示同意
    2 表示拒绝
    3 表示正则通话中
*/
function reply(targetUsername,replyType) {
  sendToServer({
    name: myUsername,
    target: targetUsername,
    id: clientID,
    replyType: replyType,
    type: "reply-video"
  });
}


// 收到回复
function handleReplyVideoMsg(msg){
  switch(msg.replyType) {
    case 1:
      // 对方同意
      startConnection(targetUsername);
      break;
    case 2:
      alert('对方拒绝了你的视频通话请求！');
      break;
    case 3:
      alert('对方正在通话中！');
      break;
  }
}


// 对方用户同意 开始连接
function startConnection(targetUsername) {
  // 调用createPeerConnection()创建RTCPeerConnection。
  log("设置连接以邀请用户: " + targetUsername);
  createPeerConnection();
  // 现在配置并创建本地流，将其附加到“预览”框(id为“local_video”)，并将其添加到RTCPeerConnection。
  log("请求访问摄像头……");
  navigator.mediaDevices.getUserMedia(mediaConstraints).then(function(localStream) {
    log("-- 获得的本地视频流");
    document.getElementById("local_video").srcObject = localStream;
    if (hasAddTrack) {
      log("-- 向RTCPeerConnection添加跟踪");
      console.log(localStream.getTracks());
      localStream.getTracks().forEach(track => {
        console.log(myPeerConnection);
        myPeerConnection.addTrack(track, localStream)
      });
    } else {
      log("-- 向RTCPeerConnection添加流");
      myPeerConnection.addStream(localStream);
    }
  }).catch(handleGetUserMediaError);
}

/*
  创建RTCPeerConnection，它知道如何与我们选择的STUN/TURN服务器通信，
  然后使用getUserMedia()找到我们的摄像机和麦克风，并将该流添加到连接中，
  以便在视频调用中使用。然后，我们配置事件处理程序来获取调用所需的通知。
*/
function createPeerConnection() {
  log("建立连接…");
  // 创建一个RTCPeerConnection，它知道如何使用我们选择的STUN服务器。
  // 如果是 本地 localhost 部署，这个可以忽略
  myPeerConnection = new RTCPeerConnection({
    "iceServers": [{
      urls: 'stun:stun.l.google.com:19302'
    }, {
        "urls": "turn:numb.viagenie.ca",
        "username": "webrtc@live.com",
        "credential": "muazkh"
    }]
    sdpSemantics:'plan-b'
  });
  // 我们有addTrack()吗?如果没有，我们将使用流代替。
  hasAddTrack = (myPeerConnection.addTrack !== undefined);
  // 为ICE协商过程设置事件处理程序。
  myPeerConnection.onicecandidate = handleICECandidateEvent;
  myPeerConnection.onremovestream = handleRemoveStreamEvent;
  myPeerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent;
  myPeerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent;
  myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
  myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent;
  // 因为addStream()和addStream事件的弃用是最近才出现的，所以如果addTrack()和track不可用，我们需要使用它们。
  if (hasAddTrack) {
    myPeerConnection.ontrack = handleTrackEvent;
  } else {
    myPeerConnection.onaddstream = handleAddStreamEvent;
  }
}

/*
  由WebRTC层调用，让我们知道何时开始(或重启)ICE协商。
  首先创建一个WebRTC报价，然后将其设置为本地媒体的描述(配置本地媒体流)，然后将描述作为报价发送给被调用方。
  这是一种提议的媒体格式、编解码器、分辨率等。
*/
function handleNegotiationNeededEvent() {
  console.log(myUsername);
  log("*** 谈判需要");
  log("---> 创建提供");
  myPeerConnection.createOffer().then(function(offer) {
    log("---> 创建要发送给远程对等点的新描述对象");
    return myPeerConnection.setLocalDescription(offer);
  })
  .then(function() {
    log("---> 发送offer给远程对等点");
    sendToServer({
      name: myUsername,
      target: targetUsername,
      type: "video-offer",
      sdp: myPeerConnection.localDescription
    });
  })
  .catch(reportError);
}

// 同意后
function handleReplyFun(msg) {
  var localStream = null;
  targetUsername = msg.name;
  // 调用createPeerConnection()创建RTCPeerConnection。
  log("开始接受来自" + targetUsername);
  createPeerConnection();
  // 我们需要将远程描述设置为接收到的SDP提供，以便我们的本地WebRTC层知道如何与调用者对话。
  navigator.mediaDevices.getUserMedia(mediaConstraints).then(function(stream) {
    // log("-- 获得的本地视频流");
    localStream = stream;
    document.getElementById("local_video").srcObject = localStream;
    if (hasAddTrack) {
      log("-- 向RTCPeerConnection添加跟踪");
      localStream.getTracks().forEach(track =>
            myPeerConnection.addTrack(track, localStream)
      );
    } else {
      log("-- Adding stream to the RTCPeerConnection");
      myPeerConnection.addStream(localStream);
    }
    reply(targetUsername, 1);
  }).catch(()=>{
    reply(targetUsername, 2);
    console.log('err : handleReplyFun');
  });
}

/*
  接受视频聊天的邀请。我们配置我们的本地设置，
  创建我们的RTCPeerConnection，获取并附加我们的本地摄像机流，
  然后创建并发送一个答案给调用者。
*/
function handleVideoOfferMsg(msg) {
  var desc = new RTCSessionDescription(msg.sdp);
  myPeerConnection.setRemoteDescription(desc).then(function(stream) {
    // log("-- 获得的本地视频流");
    // localStream = stream;
    // document.getElementById("local_video").srcObject = localStream;
    // if (hasAddTrack) {
    //   log("-- 向RTCPeerConnection添加跟踪");
    //   localStream.getTracks().forEach(track =>
    //         myPeerConnection.addTrack(track, localStream)
    //   );
    // } else {
    //   log("-- Adding stream to the RTCPeerConnection");
    //   myPeerConnection.addStream(localStream);
    // }
    log("------> 向RTCPeerConnection添加流");
    /* 现在我们已经成功地设置了远程描述，我们需要在本地启动流，然后创建一个SDP答案。
    这个SDP数据描述了我们调用的本地端，包括编解码器信息、商定的选项等等。 */
    return myPeerConnection.createAnswer();
  }).then(function(answer) {
    log("------> 创建答案后设置本地描述");
    /* 现在我们有了答案，所以把它建立为局部描述。
    这实际上配置了我们调用的结尾，以匹配SDP中指定的设置。 */
    return myPeerConnection.setLocalDescription(answer);
  }).then(function() {
    var msg = {
      name: myUsername,
      target: targetUsername,
      type: "video-answer",
      sdp: myPeerConnection.localDescription
    };
    // 我们已经配置好通话结束。是时候把我们的答案发给打电话的人了，这样他们就知道我们想和他们交谈，以及如何和我们交谈。
    log("将应答包发送回其他对等点");
    sendToServer(msg);
  }).catch(handleGetUserMediaError);
}

// 一旦被来电者决定接受我们的通话请求，立即回复发送给来电者的“视频应答”信息。
function handleVideoAnswerMsg(msg) {
  log("电话接收者已接受我们的电话");
  // 配置远程描述，这是我们的“video-answer”消息中的SDP有效负载。
  var desc = new RTCSessionDescription(msg.sdp);
  myPeerConnection.setRemoteDescription(desc).catch(reportError);
}

/*
  当事件发生在WebRTC调用的媒体轨道上时，由WebRTC层调用。这包括在调用中添加和删除流时。
  track events 包含一下目录：
    RTCRtpReceiver       receiver
    MediaStreamTrack     track
    MediaStream[]        streams
    RTCRtpTransceiver    transceiver
*/
function handleTrackEvent(event) {
  log("*** Track event");
  document.getElementById("received_video").srcObject = event.streams[0];
  document.getElementById("hangup-button").disabled = false;
}

// 当流开始从远程对等点到达时，由WebRTC层调用。在本例中，我们使用它来更新用户界面。
function handleAddStreamEvent(event) {
  log("*** Stream added");
  document.getElementById("received_video").srcObject = event.stream;
  document.getElementById("hangup-button").disabled = false;
}

// 当连接的远程端删除其流时调用的事件处理程序。我们认为这和挂断电话是一回事。它就像一个“哑巴”。
// 请注意，目前规范还不清楚这种情况和其他“连接失败”场景应该在什么时候发生，所以有时它们根本不会发生。
function handleRemoveStreamEvent(event) {
  log("*** Stream removed");
  closeVideoCall();
}

// 通过通过信令服务器将指定的ICE候选对象(由我们的本地ICE代理创建)转发给另一个对等点，处理|icecandidate|事件。
function handleICECandidateEvent(event) {
  if (event.candidate) {
    log("即将离任的冰候选人: " + event.candidate.candidate);

    sendToServer({
      type: "new-ice-candidate",
      target: targetUsername,
      candidate: event.candidate
    });
  }
}

// 处理| iceconnectionstatechange |事件。这将检测ICE连接何时关闭、失败或断开。
// 这在ICE代理的状态发生更改时调用。
function handleICEConnectionStateChangeEvent(event) {
  log("*** 冰连接状态变为 " + myPeerConnection.iceConnectionState);
  switch(myPeerConnection.iceConnectionState) {
    case "closed":
    case "failed":
    case "disconnected":
      closeVideoCall();
      break;
  }
}

// 设置|signalingstatechange|事件处理程序。这将检测信号连接何时关闭。
// 注意:这将实际移动到属性RTCPeerConnection中返回的新RTCPeerConnectionState枚举。当浏览器赶上最新版本的规范时，connectionState !
function handleSignalingStateChangeEvent(event) {
  log("*** WebRTC信令状态变为: " + myPeerConnection.signalingState);
  switch(myPeerConnection.signalingState) {
    case "closed":
      closeVideoCall();
      break;
  }
}

// 处理|icegatheringstatechange|事件。这让我们知道ICE引擎目前正在做什么:“new”表示还没有建立网络，“gather”表示ICE引擎正在收集候选对象，“complete”表示收集已经完成。注意，随着需求和环境的变化，引擎可以在“收集”和“完成”之间反复切换。
// 当这种情况发生时，我们不需要做任何事情，但是我们将它记录到控制台，这样您就可以看到在处理示例时发生了什么。
function handleICEGatheringStateChangeEvent(event) {
  log("*** ICE gathering state changed to: " + myPeerConnection.iceGatheringState);
}

/*
  关闭RTCPeerConnection并重置变量，以便用户可以在需要的时候发出或接收另一个调用。
  这在用户挂起、另一个用户挂起或检测到连接失败时调用。
*/
function closeVideoCall() {
  var remoteVideo = document.getElementById("received_video");
  var localVideo = document.getElementById("local_video");
  log("Closing the call");
  // 关闭RTCPeerConnection
  if (myPeerConnection) {
    log("--> 关闭对等连接");
    // 断开所有事件监听器;我们不希望在挂起电话的过程中出现意外事件。
    myPeerConnection.onaddstream = null;  // 对于年长的实现
    myPeerConnection.ontrack = null;      // For newer ones
    myPeerConnection.onremovestream = null;
    myPeerConnection.onnicecandidate = null;
    myPeerConnection.oniceconnectionstatechange = null;
    myPeerConnection.onsignalingstatechange = null;
    myPeerConnection.onicegatheringstatechange = null;
    myPeerConnection.onnotificationneeded = null;
    // 停止视频
    if (remoteVideo.srcObject) {
      remoteVideo.srcObject.getTracks().forEach(track => track.stop());
    }
    if (localVideo.srcObject) {
      localVideo.srcObject.getTracks().forEach(track => track.stop());
    }
    remoteVideo.src = null;
    localVideo.src = null;
    // 关闭对等连接
    myPeerConnection.close();
    myPeerConnection = null;
  }
  // 禁用挂断按钮
  document.getElementById("hangup-button").disabled = true;
  targetUsername = null;
}

// 处理“挂起”消息，如果另一个对等方挂断了电话或断开连接，则发送该消息。
function handleHangUpMsg(msg) {
  log("*** Received hang up notification from other peer");
  closeVideoCall();
}

/*
  一位新的ICE候选人已经从另一位同事那里收到。
  调用RTCPeerConnection.addIceCandidate()将其发送到本地ICE框架。
*/
function handleNewICECandidateMsg(msg) {
  var candidate = new RTCIceCandidate(msg.candidate);
  log("Adding received ICE candidate: " + JSON.stringify(candidate));
  myPeerConnection.addIceCandidate(candidate)
    .catch(reportError);
}

/*
  挂断电话
  挂断电话的方法是关闭连接的一端，然后向另一端发送一条“挂断”消息(请记住，信号是在另一个连接上完成的)。这将通知另一个对等点连接应该终止，UI返回到“no call in progress”状态。
*/
function hangUpCall() {
  closeVideoCall();
  sendToServer({
    name: myUsername,
    target: targetUsername,
    type: "hang-up"
  });
}

/*
  处理试图访问本地媒体硬件时发生的错误;
  也就是说，getUserMedia()抛出异常。
  最可能出现的两种情况是，用户没有相机和/或麦克风，或者在提示时拒绝共享设备。
  如果他们只是选择不分享他们的媒体，这并不是一个真正的错误，所以我们不会在这种情况下提供信息。
*/
function handleGetUserMediaError(e) {
  log(e);
  switch(e.name) {
    case "NotFoundError":
      alert("Unable to open your call because no camera and/or microphone" +
            "were found.");
      break;
    case "SecurityError":
    case "PermissionDeniedError":
      // 什么也不做;这与用户取消调用是一样的。
      break;
    default:
      alert("Error opening your camera and/or microphone: " + e.message);
      break;
  }
  // 确保我们关闭了RTCPeerConnection的端，以便准备再次尝试。
  closeVideoCall();
}


/*
 ====================================其它============================================
 ====================================================================================
*/
// 处理错误报告。目前，我们只是将内容转储到控制台，但在实际应用程序中，应该显示适当的(用户友好的)错误消息。
function reportError(errMessage) {
  log_error("Error " + errMessage.name + ": " + errMessage.message);
}
// 打印日志
function log(text) {
  var time = new Date();
  console.log("[" + time.toLocaleTimeString() + "] " + text);
}
// 打印错误日志
function log_error(text) {
  var time = new Date();
  console.error("[" + time.toLocaleTimeString() + "] " + text);
}
